﻿using CleanArchitecture.Core;
using CleanArchitecture.Core.Configuration;
using CleanArchitecture.Core.Events;
using CleanArchitecture.Core.Handlers;
using CleanArchitecture.Core.Helper;
using CleanArchitecture.Core.Interfaces;
using CleanArchitecture.Infrastructure.Entities;
using CleanArchitecture.Infrastructure.Entities.Business;
using CleanArchitecture.Infrastructure.Entities.Settings;
using CleanArchitecture.Infrastructure.Interfaces;
using CleanArchitecture.Infrastructure.Interfaces.Media;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;

namespace CleanArchitecture.Infrastructure.Services.Media
{
    /// <summary>
    /// Resize types
    /// </summary>
    public enum ResizeType
    {
        /// <summary>
        /// Longest side
        /// </summary>
        LongestSide,

        /// <summary>
        /// Width
        /// </summary>
        Width,

        /// <summary>
        /// Height
        /// </summary>
        Height
    }
    /// <summary>
    /// Picture service
    /// </summary>
    public partial class FileUploadSerive : IFileUploadService
    {
        #region Fields

        private readonly IDataProvider _dataProvider;
        private readonly IDbContext _dbContext;
        private readonly IDomainEventDispatcher _eventDispatcher;
        private readonly IHttpContextAccessor _httpContextAccessor;
        private readonly INowaFileProvider _fileProvider;
        private readonly IRepository<FileUpload> _fileRepository;
        private readonly IRepository<FileBinary> _fileBinaryRepository;
        private readonly IWebHelper _webHelper;
        private readonly MediaSettings _mediaSettings;
        protected readonly IWebHostEnvironment _hostingEnvironment;
        #endregion

        #region Ctor

        public FileUploadSerive(IDataProvider dataProvider,
            IDbContext dbContext,
            IDomainEventDispatcher eventDispatcher,
            IHttpContextAccessor httpContextAccessor,
            INowaFileProvider fileProvider,
            IRepository<FileUpload> fileRepository,
            IRepository<FileBinary> fileBinaryRepository,
            IWebHelper webHelper,
            MediaSettings mediaSettings,
            IWebHostEnvironment webhostEnvironment)
        {
            _dataProvider = dataProvider;
            _dbContext = dbContext;
            _eventDispatcher = eventDispatcher;
            _httpContextAccessor = httpContextAccessor;
            _fileProvider = fileProvider;
            _fileRepository = fileRepository;
            _fileBinaryRepository = fileBinaryRepository;
            _webHelper = webHelper;
            _mediaSettings = mediaSettings;
            _hostingEnvironment = webhostEnvironment;
        }

        #endregion

        #region Utilities
        /// <summary>
        /// Calculates picture dimensions whilst maintaining aspect
        /// </summary>
        /// <param name="originalSize">The original picture size</param>
        /// <param name="targetSize">The target picture size (longest side)</param>
        /// <param name="resizeType">Resize type</param>
        /// <param name="ensureSizePositive">A value indicating whether we should ensure that size values are positive</param>
        /// <returns></returns>
        protected virtual Size CalculateDimensions(Size originalSize, int targetSize,
            ResizeType resizeType = ResizeType.LongestSide, bool ensureSizePositive = true)
        {
            float width, height;

            switch (resizeType)
            {
                case ResizeType.LongestSide:
                    if (originalSize.Height > originalSize.Width)
                    {
                        // portrait
                        width = originalSize.Width * (targetSize / (float)originalSize.Height);
                        height = targetSize;
                    }
                    else
                    {
                        // landscape or square
                        width = targetSize;
                        height = originalSize.Height * (targetSize / (float)originalSize.Width);
                    }

                    break;
                case ResizeType.Width:
                    width = targetSize;
                    height = originalSize.Height * (targetSize / (float)originalSize.Width);
                    break;
                case ResizeType.Height:
                    width = originalSize.Width * (targetSize / (float)originalSize.Height);
                    height = targetSize;
                    break;
                default:
                    throw new Exception("Not supported ResizeType");
            }

            if (!ensureSizePositive)
                return new Size((int)Math.Round(width), (int)Math.Round(height));

            if (width < 1)
                width = 1;
            if (height < 1)
                height = 1;

            //we invoke Math.Round to ensure that no white background is rendered - https://www.nopcommerce.com/boards/t/40616/image-resizing-bug.aspx
            return new Size((int)Math.Round(width), (int)Math.Round(height));
        }

        /// <summary>
        /// Loads a file from file stored in local directory
        /// </summary>
        /// <param name="file"></param>
        /// <returns>fileBinary</returns>
        protected virtual byte[] LoadFileFromLocalPath(FileUpload file)
        {
            var lastPart = GetFileExtensionFromMimeType(file.MimeType);
            var fileName = $"{file.SeoFilename}.{lastPart}";
            var filePath = _fileProvider.GetAbsolutePath($"{file.VirtualPath.TrimStart('~')}", fileName);
            if (!_fileProvider.FileExists(filePath)) return null;
            return _fileProvider.ReadAllBytes(filePath);
        }

        /// <summary>
        /// Save picture on file system
        /// </summary>
        /// <param name="file">FileUpload record</param>
        /// <param name="fileBinary">file binary</param>
        protected virtual void SaveFileInLocal(FileUpload file, byte[] fileBinary)
        {
            var lastPart = GetFileExtensionFromMimeType(file.MimeType);
            var fileName = $"{file.SeoFilename}.{lastPart}";
            var filePath = _fileProvider.GetAbsolutePath($"{file.VirtualPath.TrimStart('~')}", fileName);
            if (!_fileProvider.FileExists(filePath) && fileBinary != null)
                _fileProvider.WriteAllBytes(filePath, fileBinary);
        }

        /// <summary>
        /// Delete a file on file system
        /// </summary>
        /// <param name="picture">Picture</param>
        protected virtual void DeleteFileOnFileSystem(FileUpload file)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));

            var lastPart = GetFileExtensionFromMimeType(file.MimeType);
            var fileName = $"{file.SeoFilename}.{lastPart}";
            var filePath = _fileProvider.GetAbsolutePath($"{file.VirtualPath.TrimStart('~')}", fileName);
            _fileProvider.DeleteFile(filePath);
        }

        /// <summary>
        /// 根据规则 生成文件存储本地的文件夹路径
        /// </summary>
        /// <param name="fileExtension">fileExtension</param>
        /// <returns>Directory Path</returns>
        protected virtual string GetFileStoreLocalDirectoryPath(string fileExtension)
        {
            string RootPath = string.IsNullOrEmpty(_mediaSettings.FileStroeRootPath)
                            ? MediaDefaultConfig.FileStroeRootPath
                            : _mediaSettings.FileStroeRootPath;
            FileType fileType = MimeTypes.GetSupportFileTypeByFileExt(fileExtension);
            string tempDir = _fileProvider.GetAbsolutePath(RootPath, fileType.ToString(), DateTime.Now.ToString("yyyyMMdd"));
            _fileProvider.CreateDirectory(tempDir);
            return tempDir;
        }

        /// <summary>
        /// Get file path URL 
        /// </summary>
        /// <returns></returns>
        protected virtual string GetFilesPathUrl(string dirPath = "")
        {
            var pathBase = _httpContextAccessor.HttpContext.Request.PathBase.Value ?? string.Empty;

            var filesPathUrl = $"{pathBase}/{dirPath}/";

            filesPathUrl = string.IsNullOrEmpty(filesPathUrl)
                ? _webHelper.GetServerLocation()
                : filesPathUrl;
            return "/" + filesPathUrl.TrimStart('/');
        }

        protected virtual string GetThumbLocalPath(string thumbFileName)
        {
            var thumbsDirectoryPath = _fileProvider.GetAbsolutePath(MediaDefaultConfig.ImageThumbsPath);
            var thumbFilePath = _fileProvider.Combine(thumbsDirectoryPath, thumbFileName);
            return thumbFilePath;
        }
        /// <summary>
        /// Updates the File binary data
        /// </summary>
        /// <param name="picture">The picture object</param>
        /// <param name="binaryData">The picture binary data</param>
        /// <returns>File binary</returns>
        protected virtual FileBinary UpdateFileBinary(FileUpload file, byte[] binaryData)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));

            var fileBinary = _fileBinaryRepository.Table.FirstOrDefault(b => b.FKFileUploadId == file.Id);

            var isNew = fileBinary == null;

            if (isNew)
                fileBinary = new FileBinary
                {
                    FKFileUploadId = file.Id
                };

            fileBinary.BinaryData = binaryData;

            if (isNew)
                _fileBinaryRepository.Insert(fileBinary);
            else
                _fileBinaryRepository.Update(fileBinary);

            return fileBinary;
        }

        /// <summary>
        /// Encode the image into a byte array in accordance with the specified image format
        /// </summary>
        /// <typeparam name="TPixel">Pixel data type</typeparam>
        /// <param name="image">Image data</param>
        /// <param name="imageFormat">Image format</param>
        /// <param name="quality">Quality index that will be used to encode the image</param>
        /// <returns>Image binary data</returns>
        protected virtual byte[] EncodeImage<TPixel>(Image<TPixel> image, IImageFormat imageFormat, int? quality = null)
            where TPixel : unmanaged, IPixel<TPixel>
        {
            using var stream = new MemoryStream();
            var imageEncoder = SixLabors.ImageSharp.Configuration.Default.ImageFormatsManager.FindEncoder(imageFormat);
            switch (imageEncoder)
            {
                case JpegEncoder jpegEncoder:
                    jpegEncoder.Subsample = JpegSubsample.Ratio444;
                    jpegEncoder.Quality = quality ?? _mediaSettings.DefaultImageQuality;
                    jpegEncoder.Encode(image, stream);
                    break;

                case PngEncoder pngEncoder:
                    pngEncoder.ColorType = PngColorType.RgbWithAlpha;
                    pngEncoder.Encode(image, stream);
                    break;

                case BmpEncoder bmpEncoder:
                    bmpEncoder.BitsPerPixel = BmpBitsPerPixel.Pixel32;
                    bmpEncoder.Encode(image, stream);
                    break;

                case GifEncoder gifEncoder:
                    gifEncoder.Encode(image, stream);
                    break;

                default:
                    imageEncoder.Encode(image, stream);
                    break;
            }

            return stream.ToArray();
        }

        /// <summary>
        /// Save a value indicating whether some file (thumb) already exists
        /// </summary>
        /// <param name="thumbFilePath">Thumb file path</param>
        /// <param name="thumbFileName">Thumb file name</param>
        /// <param name="mimeType">MIME type</param>
        /// <param name="binary">Picture binary</param>
        protected virtual void SaveThumb(string thumbFilePath, string thumbFileName, string mimeType, byte[] binary)
        {
            //ensure \thumb directory exists
            var thumbsDirectoryPath = _fileProvider.GetAbsolutePath(MediaDefaultConfig.ImageThumbsPath);
            _fileProvider.CreateDirectory(thumbsDirectoryPath);

            //save
            _fileProvider.WriteAllBytes(thumbFilePath, binary);
        }

        /// <summary>
        /// Delete picture thumbs
        /// </summary>
        /// <param name="picture">Picture</param>
        protected virtual void DeletePictureThumbs(FileUpload picture)
        {
            if (picture.FileType != FileType.Image) return;
            var filter = $"{picture.Id:0000000}*.*";
            var currentFiles = _fileProvider.GetFiles(_fileProvider.GetAbsolutePath(MediaDefaultConfig.ImageThumbsPath), filter, false);
            foreach (var currentFileName in currentFiles)
            {
                var thumbFilePath = GetThumbLocalPath(currentFileName);
                _fileProvider.DeleteFile(thumbFilePath);
            }
        }

        /// <summary>
        /// Gets the download binary array
        /// </summary>
        /// <param name="file">File</param>
        /// <returns>Download binary array</returns>
        protected virtual byte[] GetFileBits(IFormFile file)
        {
            using var fileStream = file.OpenReadStream();
            using var ms = new MemoryStream();
            fileStream.CopyTo(ms);
            var fileBytes = ms.ToArray();
            return fileBytes;
        }

        #endregion

        #region Getting picture local path/URL methods

        /// <summary>
        /// Returns the file extension from mime type.
        /// </summary>
        /// <param name="mimeType">Mime type</param>
        /// <returns>File extension</returns>
        public virtual string GetFileExtensionFromMimeType(string mimeType)
        {
            if (mimeType == null)
                return null;
            return MimeTypes.GetSupportFileExtensionByMimeType(mimeType);
        }

        /// <summary>
        /// Gets the loaded file binary depending on file storage settings
        /// </summary>
        /// <param name="file">FileUpload</param>
        /// <returns>File binary</returns>
        public virtual byte[] LoadFileBinary(FileUpload file)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));
            byte[] result = null;
            if (file.StoreType == EntityEnumConfig.FileStoreType.LocalStorage)
            {
                result = LoadFileFromLocalPath(file);
                if (result == null)
                {
                    result = file.FileBinary?.BinaryData ?? _fileBinaryRepository.Table.FirstOrDefault(b => b.FKFileUploadId == file.Id)?.BinaryData;
                    if (result == null) return null;
                    SaveFileInLocal(file, result);
                }
                return result;
            }
            else return null;
        }

        /// <summary>
        /// Get picture SEO friendly name
        /// </summary>
        /// <param name="name">Name</param>
        /// <returns>Result</returns>
        public virtual string GetFileSeName(string name)
        {
            if (_mediaSettings.RemainFileName) return name;
            //return CommonHelper.GetSeName(name, true, false);
            return DateTime.Now.ToString("yyyyMMddHHmmssfff") + CommonHelper.GenerateRandomDigitCode(5);
        }

        /// <summary>
        /// Gets the default file URL
        /// 当文件或图片无法找到时，使用默认文件或图片替换显示
        /// </summary>
        /// <param name="targetSize">The target file size (longest side) 当filetype为image时用，指定图片长度比例</param>
        /// <param name="defaultFileType">Default file type</param>
        /// <returns>File URL</returns>
        public virtual string GetDefaultFileUrl(FileType defaultFileType = FileType.Image, int targetSize = 0)
        {
            string defaultFileName = defaultFileType == FileType.Image
                                   ? MediaDefaultConfig.DefaultImageFileName
                                   : MediaDefaultConfig.DefaultFileName;
            string fileRoot = string.Empty;
            if (defaultFileType == FileType.Image)
                fileRoot = string.IsNullOrEmpty(_mediaSettings.DefaultImageRootPath)
                         ? MediaDefaultConfig.DefaultImageRootPath
                         : _mediaSettings.DefaultImageRootPath;
            else
                fileRoot = string.IsNullOrEmpty(_mediaSettings.FileStroeRootPath)
                            ? MediaDefaultConfig.FileStroeRootPath
                            : _mediaSettings.FileStroeRootPath;
            string filePath = _fileProvider.GetAbsolutePath(fileRoot, defaultFileName);
            if (!_fileProvider.FileExists(filePath))
            {
                return string.Empty;
            }

            if (defaultFileType != FileType.Image || (defaultFileType == FileType.Image && targetSize == 0))
            {
                var url = GetFilesPathUrl(fileRoot) + defaultFileName;

                return url;
            }
            else
            {//Image and set with targetImage size
                var fileExtension = _fileProvider.GetFileExtension(filePath);
                var thumbFileName = $"{_fileProvider.GetFileNameWithoutExtension(filePath)}_{targetSize}{fileExtension}";
                var thumbFilePath = GetThumbLocalPath(thumbFileName);
                if (!_fileProvider.FileExists(thumbFilePath))
                {
                    using Image<Rgba32> image = Image.Load<Rgba32>(filePath, out var imageFormat);
                    image.Mutate(imageProcess => imageProcess.Resize(new ResizeOptions
                    {
                        Mode = ResizeMode.Max,
                        Size = CalculateDimensions(image.Size(), targetSize)
                    }));
                    var pictureBinary = EncodeImage(image, imageFormat);
                    SaveThumb(thumbFilePath, thumbFileName, imageFormat.DefaultMimeType, pictureBinary);
                }

                var url = GetFilesPathUrl() + $"{MediaDefaultConfig.ImageThumbsPath}/{thumbFileName}";
                return url;
            }
        }

        /// <summary>
        /// Get a fileupload URL
        /// </summary>
        /// <param name="fileuploadId">Picture identifier</param>
        /// <param name="targetSize">The target file size (longest side)</param>
        /// <param name="showDefaultPicture">A value indicating whether the default picture is shown</param>
        /// <returns>fileupload URL</returns>
        public virtual string GetFileUrl(int fileuploadId,
            int targetSize = 0,
            bool showDefaultPicture = true)
        {
            var file = GetFileUploadEntityById(fileuploadId);
            return GetFileUrl(file, targetSize, showDefaultPicture);
        }

        /// <summary>
        /// Get a file URL
        /// </summary>
        /// <param name="fileupload">fileupload instance</param>
        /// <param name="targetSize">The target file size (longest side)</param>
        /// <param name="showDefaultPicture">A value indicating whether the default picture is shown</param>
        /// <returns>fileupload URL</returns>
        public virtual string GetFileUrl(FileUpload file,
            int targetSize = 0,
            bool showDefaultPicture = true)
        {
            if (file == null)
                return showDefaultPicture ? GetDefaultFileUrl(FileType.Image, targetSize) : string.Empty;

            if (file.StoreType == EntityEnumConfig.FileStoreType.QiniuCloud)
                return file.VirtualPath;

            byte[] fileBinary = null;
            if (file.IsNew == EntityEnumConfig.YesorNo.YES) //图片或文件
            {
                if (file.FileType != FileType.Video && file.FileType != FileType.Audio)
                    DeleteFileOnFileSystem(file);
                fileBinary = LoadFileBinary(file); //Load and Save file Local

                if ((fileBinary?.Length ?? 0) == 0)
                    return showDefaultPicture ? GetDefaultFileUrl(file.FileType, targetSize) : string.Empty;

                file.IsNew = EntityEnumConfig.YesorNo.No;
                file = UpdateFileEntityOnly(file);
            }
            var seoFileName = file.SeoFilename;
            var lastPart = MimeTypes.GetSupportFileExtensionByMimeType(file.MimeType);
            if (file.FileType != FileType.Image || (file.FileType == FileType.Image && targetSize == 0))
            {
                var url = GetFilesPathUrl(file.VirtualPath.TrimStart('~')) + $"{seoFileName}.{lastPart}";
                return url;
            }
            //get Image url with targetSize
            string thumbFileName = !string.IsNullOrEmpty(seoFileName)
                    ? $"{file.Id:0000000}_{seoFileName}_{targetSize}.{lastPart}"
                    : $"{file.Id:0000000}_{targetSize}.{lastPart}"; ;

            var thumbFilePath = GetThumbLocalPath(thumbFileName);

            //the named mutex helps to avoid creating the same files in different threads,
            //and does not decrease performance significantly, because the code is blocked only for the specific file.
            using (var mutex = new Mutex(false, thumbFileName))
            {
                if (_fileProvider.FileExists(thumbFilePath))
                    return GetFilesPathUrl() + $"{MediaDefaultConfig.ImageThumbsPath}/{thumbFileName}";

                mutex.WaitOne();

                //check, if the file was created, while we were waiting for the release of the mutex.
                if (!_fileProvider.FileExists(thumbFilePath))
                {
                    if ((fileBinary?.Length ?? 0) == 0)
                        return showDefaultPicture ? GetDefaultFileUrl(file.FileType, targetSize) : string.Empty;

                    byte[] pictureBinaryResized;
                    if (targetSize != 0)
                    {
                        //resizing required
                        using var image = Image.Load<Rgba32>(fileBinary, out var imageFormat);
                        image.Mutate(imageProcess => imageProcess.Resize(new ResizeOptions
                        {
                            Mode = ResizeMode.Max,
                            Size = CalculateDimensions(image.Size(), targetSize)
                        }));

                        pictureBinaryResized = EncodeImage(image, imageFormat);
                    }
                    else
                    {
                        //create a copy of pictureBinary
                        pictureBinaryResized = fileBinary.ToArray();
                    }

                    SaveThumb(thumbFilePath, thumbFileName, file.MimeType, pictureBinaryResized);
                }

                mutex.ReleaseMutex();
            }

            return GetFilesPathUrl() + $"{MediaDefaultConfig.ImageThumbsPath}/{thumbFileName}"; ;
        }

        #endregion

        #region CRUD methods
        /// <summary>
        /// Gets a file
        /// </summary>
        /// <param name="fileuploadId">FileUpload identifier</param>
        /// <returns>FileUpload</returns>
        public virtual FileUpload GetFileUploadEntityById(int fileId)
        {
            if (fileId == 0)
                return null;

            return _fileRepository.GetById(fileId);
        }

        public virtual IList<FileUpload> GetFileUploadList(int[] Ids)
        {
            if (Ids == null) return null;
            if (Ids.Length == 0) return null;
            return _fileRepository.TableNoTracking.Where(f => Ids.Contains(f.Id)).ToList();
        }

        /// <summary>
        /// Updates the File 
        /// 只更新文件记录，适用于云存储
        /// </summary>
        /// <param name="file">The file to update</param>
        /// <returns>FileUpload</returns>
        public virtual FileUpload UpdateFileEntityOnly(FileUpload file)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));
            _fileRepository.Update(file);

            //event notification
            if (!string.IsNullOrEmpty(file.SysLogMsg))
                _eventDispatcher.Dispatch(new EntityUpdatedEvent<FileUpload>(file));

            return file;
        }
        /// <summary>
        /// Deletes a file
        /// </summary>
        /// <param name="file">FileUpload</param>
        public virtual void DeleteFileUploadEntity(FileUpload file)
        {
            if (file == null)
                throw new ArgumentNullException(nameof(file));

            if (file.StoreType == EntityEnumConfig.FileStoreType.LocalStorage)
            {
                //delete thumbs if file is image
                DeletePictureThumbs(file);
                //delete from file system
                DeleteFileOnFileSystem(file);
                if (file.FileBinary == null)
                    file.FileBinary = _fileBinaryRepository.Table.FirstOrDefault(b => b.FKFileUploadId == file.Id);
            }

            //delete from database
            _fileRepository.Delete(file);

            //event notification
            if (!string.IsNullOrEmpty(file.SysLogMsg))
                _eventDispatcher.Dispatch(new EntityDeletedEvent<FileUpload>(file));
        }


        /// <summary>
        /// Gets a collection of Uploaded files
        /// </summary>
        /// <param name="fileTypes">fileTypes</param>
        /// <param name="virtualPath">Virtual path</param>
        /// <param name="pageIndex">Current page</param>
        /// <param name="pageSize">Items on each page</param>
        /// <returns>Paged list of </returns>
        public virtual IViewPagedList<FileUpload> GetFileList(FileType[] fileTypes, int pageIndex = 0, int pageSize = int.MaxValue)
        {
            var query = _fileRepository.Table;
            if (fileTypes != null)
            {
                query = query.Where(p => fileTypes.Contains(p.FileType));
            }
            query = query.OrderByDescending(p => p.UpdatedOnTimeUtc);

            var pics = new ViewPagedList<FileUpload>(query, pageIndex, pageSize);
            return pics;
        }

        /// <summary>
        /// Inserts a File
        /// 如果存储是云存储，则不用保存文件二进制
        /// </summary>
        /// <param name="fileType">The file type</param>
        /// <param name="mimeType">The file MIME type</param>
        /// <param name="seoFilename">The SEO filename</param>
        /// <param name="altAttribute">"alt" attribute for "img" HTML element</param>
        /// <param name="titleAttribute">"title" attribute for "img" HTML element</param>
        /// <param name="isNew">A value indicating whether the picture is new</param>
        /// <param name="fileBinary">The file binary, if store to cloud, then do not insert file binary</param>
        /// <param name="validateBinary">A value indicating whether to validated provided file binary</param>
        /// <param name="storeType">a value indicating file store to local or to Cloud</param>
        /// <returns>FileUpload</returns>
        public virtual FileUpload InsertFileEntity(FileType fileType, string mimeType, string category, string seoFilename, string altAttribute = null, string titleAttribute = null,
            bool isNew = true, string virtualpath = "", byte[] fileBinary = null, EntityEnumConfig.FileStoreType storeType = EntityEnumConfig.FileStoreType.LocalStorage, string linkUrl = null)
        {
            mimeType = CommonHelper.EnsureNotNull(mimeType);
            mimeType = CommonHelper.EnsureMaximumLength(mimeType, 20);
            category = CommonHelper.EnsureNotNull(category);
            category = CommonHelper.EnsureMaximumLength(category, 50);
            seoFilename = CommonHelper.EnsureMaximumLength(seoFilename, 100);
            if (storeType == EntityEnumConfig.FileStoreType.QiniuCloud && string.IsNullOrEmpty(virtualpath))
                throw new NowaException("virtual Path null when Cloud storage");
            else if (string.IsNullOrEmpty(virtualpath))
            {
                var fileExt = MimeTypes.GetSupportFileExtensionByMimeType(mimeType);
                var dirpath = GetFileStoreLocalDirectoryPath($".{fileExt}");
                virtualpath = _fileProvider.GetVirtualPath(dirpath);
            }
            if (fileType == FileType.Image)
                fileBinary = ValidatePictureDimension(fileBinary, mimeType);

            var file = new FileUpload
            {
                MimeType = mimeType,
                FileType = fileType,
                Category = category,
                SeoFilename = seoFilename,
                AltAttribute = altAttribute,
                TitleAttribute = titleAttribute,
                IsNew = isNew ? EntityEnumConfig.YesorNo.YES : EntityEnumConfig.YesorNo.No,
                VirtualPath = virtualpath,
                StoreType = storeType,
                UpdatedOnTimeUtc = DateTime.UtcNow,
                DownloadCount = 0,
                LinkUrl = linkUrl
            };
            //file.FileBinary = new FileBinary
            //{
            //    FKFileUploadId = file.Id,
            //    BinaryData = fileBinary != null ? fileBinary : new byte[0]
            //};

            _fileRepository.Insert(file);
            var binaryData = fileBinary ?? (new byte[0]);
            //DavidLee 2020/2/25 除图片 文件外 音视频直接存储本地
            if (file.FileType == FileType.Video || file.FileType == FileType.Audio)
                SaveFileInLocal(file, binaryData);
            else
                UpdateFileBinary(file, binaryData);

            //event notification
            if (!string.IsNullOrEmpty(file.SysLogMsg))
                _eventDispatcher.Dispatch(new EntityInsertedEvent<FileUpload>(file));

            return file;
        }

        /// <summary>
        /// Inserts a File 从上传文件类型自动检测文件类型
        /// 如果存储是云存储，则不用保存文件二进制
        /// </summary>
        /// <param name="formFile">Form file</param>
        /// <param name="virtualPath">Virtual path for cloudservice override</param>
        /// <param name="storeType">a value indicating file store to local or to Cloud</param>
        /// <returns>FileUpload</returns>
        public virtual FileUpload InsertFileEntity(IFormFile formFile, string category, string virtualPath = "",
                              EntityEnumConfig.FileStoreType storeType = EntityEnumConfig.FileStoreType.LocalStorage)
        {
            if (formFile.Length > _mediaSettings.FileMaximumSizeBytes)
                throw new NowaException("File is too large, recmmend using Cloud upload");
            if (storeType == EntityEnumConfig.FileStoreType.LocalStorage && formFile.Length > MediaDefaultConfig.StoreFileMaxSizeBytes)
                throw new NowaException("File is too large, recmmend using Cloud upload");
            if (string.IsNullOrEmpty(category)) throw new ArgumentNullException(nameof(category));
            var fileName = formFile.FileName;
            var contentType = formFile.ContentType;
            var fileExtension = _fileProvider.GetFileExtension(fileName);
            //remove path (passed in IE)
            fileName = GetFileSeName(_fileProvider.GetFileNameWithoutExtension(fileName));

            if (!string.IsNullOrEmpty(fileExtension))
                fileExtension = fileExtension.ToLowerInvariant();
            if (!MimeTypes.IsSupportByFileExt(fileExtension))
                throw new NowaException("File Not support for uploaad");
            if (string.IsNullOrEmpty(contentType))
                contentType = MimeTypes.GetSupportFileMimetypeByFileExt(fileExtension);
            FileType filetype = MimeTypes.GetSupportFileTypeByFileExt(fileExtension);
            var file = InsertFileEntity(filetype, contentType, category, fileName, "", "", true, "", GetFileBits(formFile), storeType: storeType);

            return file;
        }

        /// <summary>
        /// Updates the file 
        /// </summary>
        /// <param name="fileuploadId">The file identifier</param>
        /// <param name="fileBinary">The file binary</param>
        /// <param name="seoFilename">The SEO filename</param>
        /// <param name="altAttribute">"alt" attribute for "img" HTML element</param>
        /// <param name="titleAttribute">"title" attribute for "img" HTML element</param>
        /// <param name="isNew">A value indicating whether the picture is new</param>
        /// <returns>FileUpload</returns>
        public virtual FileUpload UpdateFileEntityWithBinary(int fileuploadId, byte[] fileBinary, string seoFilename, string altAttribute = null,
            string titleAttribute = null, bool isNew = true)
        {
            seoFilename = CommonHelper.EnsureMaximumLength(seoFilename, 100);

            var file = GetFileUploadEntityById(fileuploadId);
            if (file == null) return null;

            file.FileBinary = new FileBinary
            {
                FKFileUploadId = file.Id,
                BinaryData = fileBinary ?? (new byte[0])
            };
            file.SeoFilename = seoFilename;
            file.AltAttribute = altAttribute;
            file.TitleAttribute = titleAttribute;
            file.IsNew = isNew ? EntityEnumConfig.YesorNo.YES : EntityEnumConfig.YesorNo.No;
            return UpdateFileEntityWithBinary(file, seoFilename);
        }
        public virtual FileUpload UpdateFileEntityWithBinary(FileUpload file, string oldFileName = "")
        {
            if (file == null) return null;
            if (file.FileBinary == null) throw new NowaException("FileBinary cannot be null");
            if (file.FileBinary.BinaryData == null) throw new NowaException("BinaryData cannot be null");
            if (file.FileType == FileType.Image && file.FileBinary != null)
                file.FileBinary.BinaryData = ValidatePictureDimension(file.FileBinary.BinaryData, file.MimeType);
            if (!string.IsNullOrEmpty(oldFileName))
            {
                var tempName = file.SeoFilename;
                file.SeoFilename = oldFileName;
                //Delete thumbs if file is image
                DeletePictureThumbs(file);
                DeleteFileOnFileSystem(file);
                file.SeoFilename = tempName;
            }
            var fileBinary = file.FileBinary.BinaryData;
            file.FileBinary = null;
            _fileRepository.Update(file);
            //if (file.StoreType == EntityEnumConfig.FileStoreType.LocalStorage)
            //取消LazyLoading后关联关系表需要两步更新
            UpdateFileBinary(file, fileBinary);

            //event notification
            if (!string.IsNullOrEmpty(file.SysLogMsg))
                _eventDispatcher.Dispatch(new EntityUpdatedEvent<FileUpload>(file));
            return file;
        }
        /// <summary>
        /// Updates DownloadCount of a file
        /// </summary>
        /// <param name="fileuploadId">The picture identifier</param>
        /// <returns>Picture</returns>
        public virtual FileUpload SetFileDownloadCount(int fileuploadId)
        {
            var file = GetFileUploadEntityById(fileuploadId);
            if (file == null)
                throw new ArgumentException("No file found with the specified id");

            file.DownloadCount += 1;
            file.UpdatedOnTimeUtc = DateTime.UtcNow;
            file = UpdateFileEntityOnly(file);

            return file;
        }

        /// <summary>
        /// Validates input picture dimensions
        /// </summary>
        /// <param name="pictureBinary">Picture binary</param>
        /// <param name="mimeType">MIME type</param>
        /// <returns>Picture binary or throws an exception</returns>
        public virtual byte[] ValidatePictureDimension(byte[] pictureBinary, string mimeType)
        {
            using var image = Image.Load<Rgba32>(pictureBinary, out var imageFormat);
            //resize the image in accordance with the maximum size
            if (Math.Max(image.Height, image.Width) > _mediaSettings.MaximumImageSize)
            {
                image.Mutate(imageProcess => imageProcess.Resize(new ResizeOptions
                {
                    Mode = ResizeMode.Max,
                    Size = new Size(_mediaSettings.MaximumImageSize)
                }));
            }

            return EncodeImage(image, imageFormat);
        }

        /// <summary>
        /// Get pictures hashes
        /// </summary>
        /// <param name="picturesIds">Pictures Ids</param>
        /// <returns></returns>
        public IDictionary<int, string> GetPicturesHash(int[] picturesIds)
        {
            var supportedLengthOfBinaryHash = _dataProvider.SupportedLengthOfBinaryHash;
            if (supportedLengthOfBinaryHash == 0 || !picturesIds.Any())
                return new Dictionary<int, string>();

            const string strCommand = "SELECT [PictureId], HASHBYTES('sha1', substring([BinaryData], 0, {0})) as [Hash] FROM [PictureBinary] where [PictureId] in ({1})";
            return _dbContext
                .QueryFromSql<PictureHashItem>(string.Format(strCommand, supportedLengthOfBinaryHash, picturesIds.Select(p => p.ToString()).Aggregate((all, current) => all + ", " + current))).Distinct()
                .ToDictionary(p => p.PictureId, p => BitConverter.ToString(p.Hash).Replace("-", string.Empty));
        }

        /// <summary>
        /// DavidLee : 2019/12/12 Ef Core 目前不支持通过linq进行group by的查询
        /// </summary>
        /// <returns></returns>
        public virtual IDictionary<string, int> GetVirtualDirectories(string[] usecategories)
        {
            //SELECT Category, COUNT(Id)
            //FROM bus_fileupload
            //WHERE Category in ('FaceImage', 'NewsImage')
            //GROUP BY Category;
            //
            var query = from file in _fileRepository.TableNoTracking
                        where usecategories.Contains(file.Category)
                        group file.Id by file.Category into g
                        select new
                        {
                            DirName = g.Key,
                            ImageCount = g.Count()
                        };
            return query.ToDictionary(g => g.DirName, g => g.ImageCount);
            //var query = _fileRepository.Table.Where(f => usecategories.Contains(f.Category)).ToList();
            //var groupedDic = (from pic in query
            //                  group pic.VirtualPath by pic.VirtualPath)
            //                 .ToDictionary(g => g.Key, g => g.Count());
            //return groupedDic;
        }

        //public virtual Stream GetCloudImageStream(string url, string defaultImgPath)
        //{
        //    FileStream imageStream = null;
        //    if (_fileProvider.FileExists(defaultImgPath))
        //    {
        //        imageStream = new FileStream(defaultImgPath, FileMode.Open, FileAccess.Read);
        //    }
        //    return imageStream;
        //}

        //public virtual void UploadImg2Cloud(Picture imgEntity)
        //{//Do nothing
        //}
        #endregion

    }
}
